/*
 *
 * Extension of tinySQLTable which manipulates dbf files.
 *
 * Copyright 1996 John Wiley & Sons, Inc. 
 * See the COPYING file for redistribution details.
 *
 * $Author: davis $
 * $Date: 2004/12/18 21:29:47 $
 * $Revision: 1.1 $
 *
 */
using System;
using java = biz.ritter.javapi;

namespace com.sqlmagic.tinysql
{


    /**
    dBase read/write access <br> 
    @author Brian Jepson <bjepson@home.com>
    @author Marcel Ruff <ruff@swand.lake.de> Added write access to dBase and JDK 2 support
    @author Thomas Morgner <mgs@sherito.org> Added caching for the current read row. A row
    is now read only once and substrings are generated by each call to GetCol. Incredibly 
    increased read speed when little memory is available and disks are slow.
    */
    public class DBFFileTable : TinySQLTable
    {
        private String fullPath, fileName;
        private DBFHeader dbfHeader = null; // the first 32 bytes of the dBase file
        private java.io.RandomAccessFile ftbl;  // access handle to the dBase file
        public bool fileOpen = false;
        internal const String dbfExtension = ".DBF";

        // dBase III column info offsets (one for each column):
        internal const int FIELD_NAME_INDEX = 0;       // 0-10 column name, ASCIIZ - null padded
        internal const int FIELD_TYPE_INDEX = 11;      // 11-11
        internal const int IMU_INDEX = 12;             // 12-15 (in memory use)
        internal const int FIELD_LENGTH_INDEX = 16;    // 16-16 (max field length = 254)
        internal const int DECIMAL_COUNT_INDEX = 17;   // 17-17
        internal const int FIELD_RESERVED_INDEX = 18;  // 18-31

        /* 
         * The header section ends with carriage return CR.
         *
         * The data records have fixed length (from LENGTH_OF_REC_INDEX)
         * the field entries in a record have fixed length (from FIELD_LENGTH_INDEX)
         * all number and dates are stored as ASCII characters
         */
        internal const int IS_DELETED_INDEX = 0;    // '*': is deleted
        // ' ': is not deleted
        internal const char RECORD_IS_DELETED = '*';
        internal const char RECORD_IS_NOT_DELETED = ' ';
        /*
         * Current record
         */
        int currentRecordNumber = 0;      // current record, starts with 1!
        /*
         * The cache holds a single row as string and is read only once,
         * and discarded when the cursor moves
         */
        private String currentRowCache = null;
        /*
         * End of file flag
         */
        bool eof = false;
        /*
         *
         * Constructs a dbfFileTable. This is only called by getTable()
         * in dbfFile.java.
         *
         * @param dDir data directory
         * @param table_name the name of the table
         *
         */
        internal DBFFileTable(String dDir, String table_name) //throws TinySQLException 
        {
            int aliasAt;
            aliasAt = table_name.indexOf("->");
            if (aliasAt > -1)
            {
                table = table_name.substring(0, aliasAt);
                tableAlias = table_name.substring(aliasAt + 2);
            }
            else
            {
                table = table_name;
                tableAlias = table_name;
            }
            /*
             *    The full path to the file
             */
            fileName = table;
            if (!fileName.toUpperCase().endsWith(dbfExtension))
                fileName = fileName + dbfExtension;
            fullPath = dDir + java.io.File.separator + fileName;
            if (TinySQLGlobals.DEBUG)
                java.lang.SystemJ.outJ.println("dbfFileTable: fileName=" + fileName + "\nTable="
                + table + "\nfullPath=" + fullPath);
            /*
             *    Open the DBF file
             */
            column_info = open_dbf();
        }
        /*
         * Check if the file is open.
         */
        public override bool isOpen()
        {
            return fileOpen;
        }
        /*
         * Close method. Try not to call this until you are sure 
         * the object is about to go out of scope.
         */
        public override void close() //throws TinySQLException 
        {
            try
            {
                if (TinySQLGlobals.DEBUG)
                    java.lang.SystemJ.outJ.println("Closing " + toString());
                ftbl.close();
                fileOpen = false;
            }
            catch (java.io.IOException e)
            {
                throw new TinySQLException(e.getMessage());
            }
        }
        /*
         * Returns the size of a column
         *
         * @param column name of the column
         * @see tinySQLTable#ColSize
         */
        public override int ColSize(String colName) //throws TinySQLException 
        {
            TsColumn coldef = getColumn(colName);
            return coldef.size;
        }
        /*
         * Returns the number of rows in the table
         */
        public override int GetRowCount()
        {
            return dbfHeader.numRecords;
        }
        /*
         * Returns the decimal places for a column
         */
        public override int ColDec(String colName) //throws TinySQLException
        {
            TsColumn coldef = getColumn(colName);
            return coldef.decimalPlaces;
        }
        /*
         * Returns the datatype of a column.
         *
         * @param column name of the column.
         * @see tinySQLTable#ColType
         *
         * @changed to return java.sql.Types
         */
        public override int ColType(String colName) //throws TinySQLException 
        {
            TsColumn coldef = getColumn(colName);
            return coldef.type;
        }
        /*
         *  Get a column object for the named column.
         */
        public TsColumn getColumn(String colName) //throws TinySQLException
        {
            int foundDot;
            String columnName;
            columnName = colName;
            foundDot = columnName.indexOf(".");
            if (foundDot > -1)
                columnName = columnName.substring(foundDot + 1);
            columnName = TinySQLGlobals.getShortName(columnName);
            TsColumn coldef = (TsColumn)column_info.get(columnName);
            if (coldef == (TsColumn)null)
                throw new TinySQLException("Column " + columnName + " does not"
                + " exist in table " + table);
            return coldef;
        }
        /*
         * Updates the current row in the table.
         *
         * @param c Ordered Vector of column names
         * @param v Ordered Vector (must match order of c) of values
         * @see tinySQLTable#UpdateCurrentRow
         */
        public override void UpdateCurrentRow(java.util.Vector<Object> c, java.util.Vector<Object> v) //throws TinySQLException 
        {
            /*
             *    The Vectors v and c are expected to have the 
             *    same number of elements. It is also expected
             *    that the elements correspond to each other,
             *    such that value 1 of Vector v corresponds to
             *    column 1 of Vector c, and so forth.
             */
            for (int i = 0; i < v.size(); i++)
            {
                /*
                 *       Get the column name and the value, and
                 *       invoke UpdateCol() to update it.
                 */
                String column = ((String)c.elementAt(i)).toUpperCase();
                String value = (String)v.elementAt(i);
                UpdateCol(column, value);
            }
        }
        /*
         * Position the record pointer at the top of the table.
         *
         * @see tinySQLTable#GoTop
         */
        public override void GoTop() //throws TinySQLException 
        {
            currentRowCache = null;
            currentRecordNumber = 0;
            eof = false;
        }
        /*
         * Advance the record pointer to the next record.
         *
         * @see tinySQLTable#NextRecord
         */
        public override bool NextRecord() //throws TinySQLException 
        {
            currentRowCache = null;
            if (currentRecordNumber < dbfHeader.numRecords)
            {
                currentRecordNumber++;
                eof = false;
                return true;
            }
            else
            {
                eof = true;
                return false;
            }
        }
        /*
         * Insert a row. If c or v == null, insert a blank row
         *
         * @param c Ordered Vector of column names
         * @param v Ordered Vector (must match order of c) of values
         * @see tinySQLTable#InsertRow()
         *
         */
        public override void InsertRow(java.util.Vector<Object> c, java.util.Vector<Object> v) //throws TinySQLException 
        {
            try
            {
                /*
                 *       Go to the end of the file, then write out the not deleted indicator
                 */
                ftbl.seek(ftbl.length());
                ftbl.write(RECORD_IS_NOT_DELETED);
                /*
                 *       Write out a blank record
                 */
                for (int i = 1; i < dbfHeader.recordLength; i++)
                {
                    ftbl.write(' ');
                }
                int numRec = (int)dbfHeader.numRecords + 1;
                currentRecordNumber = numRec;
                dbfHeader.setNumRecords(ftbl, numRec);
            }
            catch (Exception e)
            {
                if (TinySQLGlobals.DEBUG) java.lang.SystemJ.err.println(e.toString());// e.printStackTrace();
                throw new TinySQLException(e.getMessage());
            }
            if (c != null && v != null)
                UpdateCurrentRow(c, v);
            else
                dbfHeader.setTimestamp(ftbl);
        }
        /*
         * Retrieve a column's string value from the current row.
         *
         * @param column the column name
         * @see tinySQLTable#GetCol
         */
        public override String GetCol(String colName) //throws TinySQLException
        {
            int foundDot;
            String columnName;
            columnName = colName;
            foundDot = columnName.indexOf(".");
            if (foundDot > -1)
                columnName = columnName.substring(foundDot + 1);
            TsColumn coldef = (TsColumn)column_info.get(columnName);
            if (currentRowCache == null)
                currentRowCache = _GetCol(ftbl, dbfHeader, currentRecordNumber);

            return getColumn(coldef, currentRowCache);
        }
        /*
         * Extracts a column from the given row. The row is given as a string.
         * If coldef is null, the special delete-flag is returned (Position 0 of a row).
         *
         * @param coldef the column definition, which tells what content to extract from the row
         * @param row the row as an string contains all column data
         * @returns a substring of row.
         */
        public static String getColumn(TsColumn coldef, String row)
        {
            if (row == (String)null) java.lang.SystemJ.outJ.println("Row is null");
            else if (row.length() == 0) java.lang.SystemJ.outJ.println("Row has 0 length");
            if (coldef == null)
                return row.substring(0, 1);
            return row.substring(coldef.position, coldef.position + coldef.size);
        }
        /*
         * Retrieve a column's string value from the given row and given colName
         * @param ff the file handle
         * @param colName the column name
         * @param the wanted record (starts with 1)
         * @see tinySQLTable#GetCol
         *
         * @author Thomas Morgner <mgs@sherito.org> This function retrieves a
         * row, perhaps the name should changed to reflect the new function.
         */
        public static String _GetCol(java.io.RandomAccessFile ff, DBFHeader dbfHeader,
           int currentRow) //throws TinySQLException 
        {
            try
            {
                /*
                 *       Seek the starting offset of the current record,
                 *       as indicated by currentRow
                 */
                ff.seek(dbfHeader.headerLength + (currentRow - 1) * dbfHeader.recordLength);
                /*
                 *       Fully read a byte array out to the length of the record and convert
                 *       it into a String.
                 */
                byte[] b = new byte[dbfHeader.recordLength];
                ff.readFully(b);
                return new java.lang.StringJ(b, Utils.encode); // "Cp437"
            }
            catch (Exception e)
            {
                throw new TinySQLException(e.getMessage());
            }
        }
        /*
         * Update a single column.
         *
         * @param column the column name
         * @param value the String value with which update the column
         * @see tinySQLTable#UpdateCol
         *
         */
        public override void UpdateCol(String colName, String value) //throws TinySQLException
        {
            String shortColumnName;
            try
            {
                /*
                 *       If it's the pseudo column _DELETED, return
                 */
                if (colName.equals("_DELETED")) return;
                /*
                 *       Retrieve the TsColumn object which corresponds to this column.
                 */
                shortColumnName = TinySQLGlobals.getShortName(colName);
                TsColumn column = (TsColumn)column_info.get(shortColumnName);
                if (column == null)
                    throw new TinySQLException("Can't update field=" + colName);
                if (Utils.isDateColumn(column.type))
                {
                    /*
                     *          Convert non-blank dates to the standard YYYYMMDD format.
                     */
                    if (value.trim().length() > 0)
                        value = UtilString.dateValue(value);
                }
                /*
                 *       Seek the starting offset of the current record,
                 *       as indicated by currentRecordNumber
                 */
                ftbl.seek(dbfHeader.headerLength + (currentRecordNumber - 1) * dbfHeader.recordLength + column.position);
                /*
                 *       Enforce the correct column length, transform to byte and write to file
                 */
                value = Utils.forceToSize(value, column.size, " ");
                byte[] b = value.getBytes(Utils.encode);
                ftbl.write(b);
                dbfHeader.setTimestamp(ftbl);
            }
            catch (Exception e)
            {
                throw new TinySQLException(e.getMessage());
            }
        }
        /*
         * Delete the current row.
         *
         * @see tinySQLTable#DeleteRow
         *
         */
        public override void DeleteRow() //throws TinySQLException
        {
            try
            {
                ftbl.seek(dbfHeader.headerLength + (currentRecordNumber - 1) * dbfHeader.recordLength);
                ftbl.write(RECORD_IS_DELETED);
            }
            catch (Exception e)
            {
                throw new TinySQLException(e.getMessage());
            }
        }
        /* 
         * Is the current row deleted?
         *
         * @see tinySQLTable#isDeleted()
         */
        public override bool isDeleted() //throws TinySQLException
        {
            return ((GetCol("_DELETED")).charAt(0) == RECORD_IS_DELETED); // "*";
        }
        /*
         * Checks whether the row is deleted. 
         */
        public static bool isDeleted(java.io.RandomAccessFile ff, DBFHeader dbfHeader, int currentRow) //throws TinySQLException
        {
            char del = _GetCol(ff, dbfHeader, currentRow).charAt(0); // "_DELETED"
            return del == RECORD_IS_DELETED;
        }
        /*
         * Check if record is marked as deleted
         * @param record the record string (the first byte '*' marks a deleted record)
         */
        public static bool isDeleted(String record)
        {
            if (record.charAt(IS_DELETED_INDEX) == RECORD_IS_DELETED)
                return true;  // '*'
            return false;   // ' '
        }
        /***************************************************************************
         *
         * End methods implemented from tinySQLTable.java
         * the rest of this stuff is private methods
         * for dbfFileTable.
         *
         * @return Length in bytes of one row or 0 if not known
         */
        public override int getRecordLength()
        {
            return dbfHeader.recordLength;
        }
        public String toString()
        {
            java.lang.StringBuffer outputBuffer;
            outputBuffer = new java.lang.StringBuffer();
            outputBuffer.append("Table " + table + ", path " + fullPath
            + ", file " + ftbl.toString());
            return outputBuffer.toString();
        }
        /*
         * opens a DBF file. This is based on Pratap Pereira's 
         * Xbase.pm perl module
         * @return column definition list (java.util.Hashtable<Object,Object>)
         *
         * @author Thomas Morgner <mgs@sherito.org> added check for
         * file exists, before the file is opened. Opening a non existing
         * file will create a new file, and we get errors while trying
         * to read the non-existend headers
         */
        java.util.Hashtable<Object, Object> open_dbf() //throws TinySQLException 
        {
            try
            {
                java.io.File f = new java.io.File(fullPath);
                if (TinySQLGlobals.DEBUG)
                    java.lang.SystemJ.outJ.println("Try to open  " + f.getAbsolutePath());
                if (!f.exists())
                {
                    throw new TinySQLException("Unable to open " + f.getAbsolutePath()
                    + " - does not exist. or can't be read.");
                }
                else if (!f.canRead())
                {
                    throw new TinySQLException("Unable to open " + f.getAbsolutePath()
                    + " - file can't be read (permissions?).");
                }
                if (f.canWrite())
                {
                    ftbl = new java.io.RandomAccessFile(f, "rw");
                }
                else
                {
                    /*
                     *          Open readonly if the file is not writeable. Needed for
                     *          databases on CD-Rom
                     */
                    ftbl = new java.io.RandomAccessFile(f, "r");
                }
                /*
                 *       Read the first 32 bytes ...
                 */
                dbfHeader = new DBFHeader(ftbl);
                /*
                 *       read the column info (each is a 32 byte bulk) ...
                 */
                java.util.Hashtable<Object, Object> coldef_list = new java.util.Hashtable<Object, Object>();
                columnNameKeys = new java.util.Vector<Object>();
                int locn = 0; // offset of the current column
                for (int i = 1; i <= dbfHeader.numFields; i++)
                {
                    TsColumn coldef = DBFFile.readColdef(ftbl, table, i, locn);
                    locn += coldef.size; // increment locn by the length of this field.
                    coldef_list.put(coldef.name, coldef);
                    columnNameKeys.addElement(coldef.name);
                }
                fileOpen = true;
                return coldef_list;
            }
            catch (Exception e)
            {
                if (TinySQLGlobals.DEBUG) java.lang.SystemJ.err.println(e.ToString());// e.printStackTrace();
                throw new TinySQLException(e.getMessage());
            }
        }

        protected internal bool isEof() {
            return this.eof;
        }
    }
}